Skip to content

feat(cli): make optional-plugin loading intent-driven, fail-fast on declared-but-missing (#1597)#3228

Merged
os-zhuang merged 2 commits into
mainfrom
claude/optional-plugin-intent-driven-fh2azp
Jul 18, 2026
Merged

feat(cli): make optional-plugin loading intent-driven, fail-fast on declared-but-missing (#1597)#3228
os-zhuang merged 2 commits into
mainfrom
claude/optional-plugin-intent-driven-fh2azp

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #1597. Follow-up to #1595 (which fixed the Cannot find package false alarm) and the deeper design problem it exposed.

Problem

os serve auto-loaded the optional AI service plugins (@objectstack/service-ai, @objectstack/service-ai-studio) by speculatively importing the package and swallowing the error, conflating two distinct questions:

  1. "is the package installed?" — presence detection via try-import
  2. "does this app actually want AI?" — intent

Consequences: installing a dependency silently auto-enabled a runtime feature; nothing ever failed loud (an app that genuinely requires AI but ships without the package booted "successfully" in a broken state); and the single catch couldn't distinguish "intentionally absent" from "plugin crashed at startup" — the string-matching that caused #1595.

Design — intent-driven, three states

Enablement is now driven by declared intent, resolved deterministically at startup. Tier gating stays an orthogonal deny.

app declares package present behavior
required (requires: ['ai' | 'ai-studio']) yes load
required no throw — fail startup (fail-fast)
auto (declared in app package.json) yes load (opt-in convenience)
auto no skip — no speculative import
off (not declared / tier denies) never load

Key changes

  • Serve.resolveOptionalPluginLoad({ tierAllowed, required, declared }) — a pure, unit-tested resolver returning 'required' | 'auto' | 'off'. Both the AIService and AIStudio guards map over it.
  • Fail-fast for required capabilities. A capability the app explicitly declared in requires whose provider package is missing (or whose plugin throws while starting) now aborts boot with a clear message (the outer boot catch prints it and exits 1) instead of console.warn-ing and booting degraded. Declared caps are snapshotted before the platform auto-injects its convenience defaults (ALWAYS_ON, mcp, pinyin-search, auth→email, queue/job), which stay best-effort.
  • AI Studio gets a first-class requires: ['ai-studio'] required path (maps to the ai tier; implies the base service). An undeclared Studio is no longer speculatively imported at all.
  • No more message-matching to tell missing from crashed. Consolidated into one Serve.isModuleNotFoundError (checks err.code first — the perf(build): OS_SKIP_DTS gating + fix optional AI plugin "Cannot find package" skip #1595 fix), replacing every duplicated string-match in serve.ts (AI ×2, capability resolver, auth, datasource ×2).
  • Spec: documented the intent / fail-fast contract on the requires field (stack.zod.ts).

Constraint preserved — apps/cloud boots clean (cloud#107)

apps/cloud ships no AI Studio and must boot clean. It declares neither the Studio package nor requires: ['ai-studio'], so Studio resolves to off → skipped, no import, no error. This is strictly better than before: previously an app declaring only the base service still speculatively imported Studio and swallowed the failure.

Why this is CI-safe

@objectstack/cli depends on every built-in capability provider package (service-automation, plugin-approvals, service-job, service-messaging, service-package, trigger-*, …), so bare import(spec.pkg) always resolves wherever the CLI runs. The example apps' declared capabilities all resolve today, so fail-fast never wrongly fires. service-ai / service-ai-studio are external/private — exactly where declared-but-missing should fail fast.

Verification

  • tsc -p packages/cli/tsconfig.build.json --noEmitexit 0
  • New packages/cli/test/serve-optional-plugin-intent.test.ts (intent resolver + isModuleNotFoundError perf(build): OS_SKIP_DTS gating + fix optional AI plugin "Cannot find package" skip #1595 regression guard) → 9 passing
  • serve-defaults, serve-host-config, serve-log-level, serve-automation-summary, adr-0048-app-splitall passing
  • packages/spec stack.test.ts89 passing; full spec build succeeds
  • eslint on changed files → clean

🤖 Generated with Claude Code


Generated by Claude Code

…1597)

`os serve` auto-loaded the optional AI service plugins by speculatively
importing the package and swallowing the error, conflating two things:
"is the package installed?" (presence) and "does this app want AI?" (intent).
Nothing ever failed loud — an app that genuinely REQUIRES AI but ships without
the package booted "successfully" in a broken state, and the single catch
couldn't tell "intentionally absent" from "plugin crashed at startup" (the
#1595 false-alarm class).

Drive enablement from declared INTENT instead, resolved deterministically at
startup into three states (tier gating stays an orthogonal deny):

  required (requires: ['ai'|'ai-studio'])  load; missing/broken => fail-fast (throw, exit 1)
  auto     (package declared in app pkg)   best-effort load
  off      (neither, or tier denies)       skip, with NO speculative import

- AIService + AIStudio guards resolve via `Serve.resolveOptionalPluginLoad`
  (pure, unit-tested). Studio gains a `requires: ['ai-studio']` required path
  (opens the `ai` tier, implies the base service).
- The `requires: [...]` capability resolver now fails fast for capabilities the
  app EXPLICITLY declared; platform-injected defaults (ALWAYS_ON, mcp, ...) stay
  best-effort. Declared caps are snapshotted before auto-injection.
- Missing-vs-crashed detection consolidated into one `Serve.isModuleNotFoundError`
  (checks err.code first — the #1595 fix), replacing the duplicated string-matches.
- apps/cloud constraint preserved: no AI Studio declared => skipped, clean boot
  (cloud#107); an undeclared Studio is no longer speculatively imported.

Spec: document the intent / fail-fast contract on `requires`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TjHfkKmEvgk8v7N8nTe5sH
@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 18, 2026 4:48pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/spec.

108 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via packages/cli, @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/cli, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/cli.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…#1597)

`os start` "What it boots" listed `requires: [...]` auto-registration but
predated the intent-driven fail-fast behavior. Clarify that a declared
service capability whose provider package isn't installed aborts boot
(fail-fast), and call out that `auth`/`ui` are the tier-gated exceptions
with their own opt-in rules.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TjHfkKmEvgk8v7N8nTe5sH
@os-zhuang
os-zhuang marked this pull request as ready for review July 18, 2026 15:58
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jul 18, 2026
@os-zhuang
os-zhuang merged commit 1b733db into main Jul 18, 2026
16 of 17 checks passed
@os-zhuang
os-zhuang deleted the claude/optional-plugin-intent-driven-fh2azp branch July 18, 2026 16:08
os-zhuang pushed a commit that referenced this pull request Jul 18, 2026
…he MCP endpoint on `os dev` boot (#3167)

#3167 PR-B — the direction-ratified, non-overlapping half. The HTTP
identity-admission e2e proof landed separately (#3228: showcase-based,
HIGH-RISK); this rebases onto it and keeps only what that PR did not cover.

Decision 2 (off-switch semantics): the MCP HTTP surface and the long-lived
stdio transport shared one env var — OS_MCP_SERVER_ENABLED=true turned HTTP on
AND silently auto-started the UNSCOPED stdio bridge (raw services, no
per-request principal). Split them:
- types: new resolveMcpStdioAutoStart(); stdio is now OS_MCP_STDIO_ENABLED
  (default off). OS_MCP_SERVER_ENABLED governs only HTTP. Legacy
  OS_MCP_SERVER_ENABLED=true still starts stdio for one release, flagged
  deprecated (=false only ever gated HTTP, unchanged).
- mcp: plugin.start() gates stdio on the new switch + warns once on the legacy
  alias. The bridgeResources(...) line (mcp-stdio-authority probe key) is
  untouched; the matrix row's env-var references are synced.

Decision 3 (dev affordance): `os dev` prints the MCP endpoint, skill URL, and a
ready-to-paste `claude mcp add` command on boot, gated on isMcpServerEnabled()
(opted-out deployments advertise nothing).

Decision 4 (exposure policy): documented the current default (non-system
objects + ai.exposed actions over the ~10-tool spine) as the committed v1
policy in connect-mcp.mdx, with the metadata-authorable direction noted.

Also: env-var docs updated for the split (+ deprecation callout); the blank
scaffold README documents the serve-side MCP surface; unit tests pin the new
switch semantics (canonical / legacy-deprecated / footgun-closed). Changeset
covers types+mcp+cli (minor) and create-objectstack (patch).

Refs #3167, #3228, ADR-0096, ADR-0097.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115eg8dAaCfWaDYYAm3ma36
os-zhuang added a commit that referenced this pull request Jul 18, 2026
…t UX + exposure-policy docs (#3217)

Decouple the MCP stdio auto-start switch (OS_MCP_STDIO_ENABLED) from the default-on HTTP surface (OS_MCP_SERVER_ENABLED), closing the footgun where =true silently attached an unscoped stdio bridge; print the MCP endpoint + connect command on `os dev` boot; document the v1 exposure policy and the env-var split.

The HTTP identity-admission e2e proof landed separately via #3228; this is the de-duplicated remainder of #3167 PR-B.

Refs #3167, #3228, ADR-0096, ADR-0097.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make optional-plugin loading intent-driven: fail-fast on declared-but-missing, drop presence-based auto-enable

2 participants